併發運算就是多線程運算,且併發(concurrency)並非併行(Parallelism)
雖然兩者從中文字面十分相似,但意義完全不同。
併發是共享時間運算,在一段時間內輪流享有時間資源。併行是平行運算,在一段時間都能享有時間資源。併發是把時間切成很多小段,在這小段時間內先後執行多項任務。併行則是透過多核心,同時處理多個任務。以譬喻來說做兩件事
併發: 一個人在一段時間做兩件事。併行:兩個人同事在做一件事。Goroutines 是輕量級的線程
而 main func 則是程式當前最主要的goroutine。
go function()
Go的併發會用到多個核心下去執行,試著執行以下的程式看看:
package main
import (
	"fmt"
	"time"
)
func main() {
	print1()
	print2()
	time.Sleep(time.Second)
}
func print1() {
	for i := 0; i < 100; i++ {
		fmt.Print("O")
	}
}
func print2() {
	for i := 0; i < 100; i++ {
		fmt.Print("X")
	}
}
運行後可得以下結果
OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
再加上關鍵字go之後
package main
import (
	"fmt"
	"time"
)
func main() {
	go print1()
	go print2()
	time.Sleep(time.Second)
}
func print1() {
	for i := 0; i < 100; i++ {
		fmt.Print("O")
	}
}
func print2() {
	for i := 0; i < 100; i++ {
		fmt.Print("X")
	}
}
運行後可得以下結果
OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOXXXXXXXXXXXOOOOX
輸出是一段一段的,一下O一下X
交錯著,代表兩邊的線程都很努力的想噴射把值Print出來。
由上面範例我們可以得知
print1()執行完後才執行print2(),並無併發情況發生。go關鍵字後執行,從結果上看來是有併發的。runtime.GOMAXPROCS(n)這一參數限制程式執行時 CPU用到的最大核心數量。
如果設置小於1,等於沒設,預設值是電腦核心數。
package main
import (
	"fmt"
	"time"
  "runtime"
)
func main() {
  runtime.GOMAXPROCS(2)
	go print1()
	go print2()
	time.Sleep(time.Second)
}
func print1() {
	for i := 0; i < 100; i++ {
		fmt.Print("O")
	}
}
func print2() {
	for i := 0; i < 100; i++ {
		fmt.Print("X")
	}
}
表示設定為2核心
Panic 是發生了預期之外的事情,導致異常、錯誤的產生,退出程序的同時回傳錯誤代碼2 (Process finished with exit code 2)。我們可以透過panic的func來主動引起錯誤發生。
要注意的是若在併發線程中發生了panic,也會導致主程式也異常結束。
package main
import (
	"fmt"
	"time"
)
func main() {
	fmt.Println("Start")
	go p()
	time.Sleep(time.Second * 1)
	fmt.Println("End")
}
func p() {
	fmt.Println("Going to crash")
	panic("Crash!")
}
執行後得以下結果
Start
Going to crash
panic: Crash!
goroutine 34 [running]:
main.p()
	/tmp/sandbox3289633550/prog.go:18 +0x65
created by main.main
	/tmp/sandbox3289633550/prog.go:11 +0x65
雖然goroutine相當的便利,但不慎使用也會引發許多問題,最常見的就是Race Condition。
以下範例中,使用了10000個被併發出去的func,每個func只做一件事:count++。
package main
import (
	"fmt"
	"time"
)
var count = 0
func main() {
	for i := 0; i < 10000; i++ {
		go race()
	}
	time.Sleep(time.Millisecond * 100)
	fmt.Println(count)
}
func race() {
	count++
}
運行後得以下結果
9508
此時我們可以發現結果並非為10000,因為多個線程同時在爭奪資源,導致有許多的數字都被重複執行了。
這種情況該如何對付呢?
Ans: 互斥鎖,再多執行緒編成中,在對公共資源進行讀寫時,必須上鎖防止其他線程爭搶資源,並在結束讀寫時在解鎖,讓其他線程知道該資源已被釋放。
If the lock is already in use, the calling goroutine blocks until the mutex is available.
為了保證total.value += i的原子性,我們通過sync.Mutex的鎖來保證該語句在同一時間只被單一線程goroutine所訪問。
package main
import (
	"fmt"
	"time"
	"sync"
)
var count = 0
var m sync.Mutex
func main() {
	for i := 0; i < 10000; i++ {
		go race()
	}
	time.Sleep(time.Millisecond * 100)
	fmt.Println(count)
}
func race() {
	m.Lock()
	count++
	m.Unlock()
}
運行後可得以下結果
10000
只要在變數前上鎖(Lock),在解鎖(Unlock)前 只有該線程能對其進行操作。
這章節教大家最基本的goroutine使用,讓大家在一些需要大量併發的情況下,能夠使用最基本的goroutine來解決,那下一章節會針對goroutine在更深入的去介紹,下個章節我們也會圍繞著sync這個標準庫去解說。